概要
本文將介紹如何用C++實現將角色根據地面傾斜度傾斜的處理。
如果你想用Blueprint來實現,請參考這篇文章。 UE4 根據地面傾斜度傾斜角色
環境
- Rider 2024.2.6
- Unreal Engine 5.4
參考資料
- https://www.youtube.com/watch?v=1ICBWJ7srxQ
- UE4 根據地面傾斜傾斜角色
- 向量叉積: https://zh.wikipedia.org/wiki/%E5%8F%89%E7%A9%8D
- 向量點積: https://zh.wikipedia.org/wiki/%E9%BB%9E%E7%A9%8D
- 翻滾、俯仰、偏航
本篇
當角色走在斜坡上時,如果不調整傾斜度會變成這樣。 角色的頭部陷入斜坡,看起來不太自然。
那麼,讓我們開始用C++來解決吧。
步驟如下
- 對著角色正下方進行射線檢測(Raycast)
- 如果射線檢測撞到地面(斜坡),則獲取地面(斜坡)的法線
- 利用獲得的法線來計算地面(斜坡)的傾斜度
- 根據地面(斜坡)的傾斜度來旋轉角色
在玩家Class中編寫AlignFloor()
函數
AlignFloor()
每0.1秒透過計時器呼叫一次(也可以在Tick中呼叫,但考慮到優化設置為0.1秒,從外觀上看,0.1秒的頻率應該不會造成明顯的違和感)。
PlayerCharacter.h1private: 2 void AlignFloor() const; 3 4 FTimerHandle AlignFloorTimerHandle;
PlayerCharacter.cpp1 2void APlayerCharacter::BeginPlay() 3{ 4 Super::BeginPlay(); 5 6 GetWorldTimerManager().SetTimer(AlignFloorTimerHandle, this, &APlayerCharacter::AlignFloor, 0.1f, true); 7} 8 9void APlayerCharacter::AlignFloor() const 10{ 11 const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector; 12 const FVector MeshDownLocation = MeshLocation - 1000.f * FVector::UpVector; 13 FHitResult HitResult; 14 FCollisionQueryParams CollisionQueryParams; 15 CollisionQueryParams.AddIgnoredActor(this); 16 const bool IsHit = GetWorld()->LineTraceSingleByChannel(HitResult, MeshLocation, MeshDownLocation, ECC_WorldStatic, 17 CollisionQueryParams); 18 if (IsHit) 19 { 20 FVector FloorNormal = HitResult.ImpactNormal; 21 FVector RightVector = GetActorRightVector(); 22 FVector UpVector = GetActorUpVector(); 23 float SlopePitch; 24 float SlopeRoll; 25 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll); 26 SlopePitch = -SlopePitch; 27 const float MeshYaw = GetMesh()->GetComponentRotation().Yaw; 28 const float MeshPitch = GetMesh()->GetComponentRotation().Pitch; 29 const FRotator FloorRotation = FRotator(MeshPitch, MeshYaw, SlopePitch); 30 GetMesh()->SetWorldRotation(FloorRotation); 31 } 32}
這樣就完成了根據地面傾斜度傾斜角色的實現!
解釋
對著角色正下方進行射線檢測
PlayerCharacter.cpp1void APlayerCharacter::AlignFloor() const 2{ 3 const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector; 4 const FVector MeshDownLocation = MeshLocation - 1000.f * FVector::UpVector; 5 FHitResult HitResult; 6 FCollisionQueryParams CollisionQueryParams; 7 CollisionQueryParams.AddIgnoredActor(this); 8 const bool IsHit = GetWorld()->LineTraceSingleByChannel(HitResult, MeshLocation, MeshDownLocation, ECC_WorldStatic, 9 CollisionQueryParams); 10 if (IsHit) 11 { 12 FVector FloorNormal = HitResult.ImpactNormal; 13 FVector RightVector = GetActorRightVector(); 14 FVector UpVector = GetActorUpVector(); 15 float SlopePitch; 16 float SlopeRoll; 17 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll); 18 SlopePitch = -SlopePitch; 19 const float MeshYaw = GetMesh()->GetComponentRotation().Yaw; 20 const float MeshPitch = GetMesh()->GetComponentRotation().Pitch; 21 const FRotator FloorRotation = FRotator(MeshPitch, MeshYaw, SlopePitch); 22 GetMesh()->SetWorldRotation(FloorRotation); 23 } 24}
這部分是從角色的稍微上方來向正下方進行射線檢測。
const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector;
+ 1.f
是為了確保與地面之間的距離。如果不這樣做,角色可能會和地面位於同一高度,導致射線檢測無法正確接觸到地面(這在我的環境中發生過)。
如果射線檢測擊中地面,則獲取地面的法線(FloorNormal)
利用獲取的法線來計算地面的傾斜度
PlayerCharacter.cpp1void APlayerCharacter::AlignFloor() const 2{ 3 const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector; 4 const FVector MeshDownLocation = MeshLocation - 1000.f * FVector::UpVector; 5 FHitResult HitResult; 6 FCollisionQueryParams CollisionQueryParams; 7 CollisionQueryParams.AddIgnoredActor(this); 8 const bool IsHit = GetWorld()->LineTraceSingleByChannel(HitResult, MeshLocation, MeshDownLocation, ECC_WorldStatic, 9 CollisionQueryParams); 10 if (IsHit) 11 { 12 FVector FloorNormal = HitResult.ImpactNormal; 13 FVector RightVector = GetActorRightVector(); 14 FVector UpVector = GetActorUpVector(); 15 float SlopePitch; 16 float SlopeRoll; 17 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll); 18 SlopePitch = -SlopePitch; 19 const float MeshYaw = GetMesh()->GetComponentRotation().Yaw; 20 const float MeshPitch = GetMesh()->GetComponentRotation().Pitch; 21 const FRotator FloorRotation = FRotator(MeshPitch, MeshYaw, SlopePitch); 22 GetMesh()->SetWorldRotation(FloorRotation); 23 } 24}
這部分的數學細節稍微複雜,所以有興趣的話可以繼續閱讀,對數學不太感興趣的可以跳過。
假設角色是一隻老鼠,想像一下這隻老鼠站在斜坡上的情況。當射線檢測擊中斜坡時,我們會獲取到它的法線。
獲取地面(斜坡)的法線。
FVector FloorNormal = HitResult.ImpactNormal;
法線是什麼?
在曲面上一個點的法線,是與該點的切平面垂直的直線。
計算傾斜角度
獲取角色的右方向向量和上方向向量。
1 //... 2 FVector RightVector = GetActorRightVector(); 3 FVector UpVector = GetActorUpVector(); 4 //...
我們使用 UKismetMathLibrary
的函數來獲取斜坡的傾斜角度(SlopePitch)。
UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll);
如果查看這個函數的內部實現,會看到以下的計算過程。
KismetMathLibary.cpp1void UKismetMathLibrary::GetSlopeDegreeAngles(const FVector& MyRightYAxis, const FVector& FloorNormal, const FVector& UpVector, float& OutSlopePitchDegreeAngle, float& OutSlopeRollDegreeAngle) 2{ 3 const FVector FloorZAxis = FloorNormal; 4 const FVector FloorXAxis = MyRightYAxis ^ FloorZAxis; 5 const FVector FloorYAxis = FloorZAxis ^ FloorXAxis; 6 7 OutSlopePitchDegreeAngle = 90.f - FMath::RadiansToDegrees(FMath::Acos(FloorXAxis | UpVector)); 8 OutSlopeRollDegreeAngle = 90.f - FMath::RadiansToDegrees(FMath::Acos(FloorYAxis | UpVector)); 9}
用圖來解釋可能會更容易理解。
例如,當老鼠站在斜坡上時,右側面圖如下所示:
△三角形是老鼠。
FloorZ
是斜坡的法線(法線向量)。FloorX
是老鼠的右方向向量和FloorZ
(法線向量)的叉積(Cross Product),結果是斜坡的上升方向向量。
在 FVector 中,插入符號(Caret)「^」是叉積的運算符。
兩個向量的叉積結果是垂直於這兩個向量的向量。
叉積詳情:https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E7%A9%8D
接下來,計算 FloorZ
和 FloorX
的叉積,可以得到「老鼠的右方向向量」。
const FVector FloorYAxis = FloorZAxis ^ FloorXAxis;
然後計算斜坡的上升方向向量(FloorX
)和老鼠的垂直向上方向向量(Up
)的點積(Dot Product),再通過對結果取 Arccos,得到角度 a。從 90 度中減去這個角度,就能獲得斜坡的傾斜角度(SlopePitch
)。
OutSlopePitchDegreeAngle = 90.f - FMath::RadiansToDegrees(FMath::Acos(FloorXAxis | UpVector));
在 FVector 中,「|」是點積的運算符。
點積詳情:https://ja.wikipedia.org/wiki/%E3%83%89%E3%83%83%E3%83%88%E7%A9%8D 點積的公式是:若向量 u 和 v 之間的角度為 θ,則有以下公式: u⋅v=∣u∣∣v∣cosθ
根據地面傾斜的量來旋轉角色
PlayerCharacter.cpp1void APlayerCharacter::AlignFloor() const 2{ 3 const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector; 4 const FVector MeshDownLocation = MeshLocation - 1000.f * FVector::UpVector; 5 FHitResult HitResult; 6 FCollisionQueryParams CollisionQueryParams; 7 CollisionQueryParams.AddIgnoredActor(this); 8 const bool IsHit = GetWorld()->LineTraceSingleByChannel(HitResult, MeshLocation, MeshDownLocation, ECC_WorldStatic, 9 CollisionQueryParams); 10 if (IsHit) 11 { 12 FVector FloorNormal = HitResult.ImpactNormal; 13 FVector RightVector = GetActorRightVector(); 14 FVector UpVector = GetActorUpVector(); 15 float SlopePitch; 16 float SlopeRoll; 17 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll); 18 SlopePitch = -SlopePitch; 19 const float MeshYaw = GetMesh()->GetComponentRotation().Yaw; 20 const float MeshPitch = GetMesh()->GetComponentRotation().Pitch; 21 const FRotator FloorRotation = FRotator(MeshPitch, MeshYaw, SlopePitch); 22 GetMesh()->SetWorldRotation(FloorRotation); 23 } 24}
通過將老鼠的 Mesh 的滾轉(Roll)反向旋轉,使角色自然地適應斜坡的傾斜。
結果
你可以在以下的演示中看到角色正確地適應斜坡並運行的情況。
最後
本文介紹了如何根據地面的傾斜來旋轉角色。如果有任何錯誤,請隨時在評論中告訴我們。